/*
 * Copyright (c) 2014, Hsu Shih-Chia (schsu01@gmail.com)
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qood;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * QUtil provides some beta-version methods which might be deprecated in the future.
 *
 * @author SCHsu01
 * @version 1.0
 */
public final class QUtil {

  private QUtil() {/*call static method only*/}

  /**
   * Create single column QModel
   *
   * @param rows column data
   * @param name column name
   * @param type column type
   * @return created QModel
   */
  public static QModel createModel(Object[] rows, String name, Class type) {
    return new ModelCol(rows.length, new Col[]{Col.getValues(type, rows)}, name);
  }

  /**
   * Check if QModel is null or no data
   *
   * @param model QModel for checking
   * @return is null or no data
   */
  public static boolean isEmpty(QModel model) {
    return null == model || 0 == model.getRowCount() || 0 == model.getColCount();
  }

  /**
   * Check if one of QModels is null or no data
   *
   * @param models QModels for checking
   * @return one of them is null or no data
   */
  public static boolean isEmpty(QModel... models) {
    for (final QModel model : models) if (isEmpty(model)) return true;
    return false;
  }

  /**
   * Check if two QModels have the same content
   *
   * @param a        the first QModel
   * @param b        the second QModel
   * @param colNames specified column name to compare, if null then use all columns in the first QModel
   * @return whether have the same content
   */
  public static boolean isEquals(QModel a, QModel b, String... colNames) {
    if (a == b) return true;
    if (isEmpty(a, b)) return false;
    final int rows = a.getRowCount();
    if (rows != b.getRowCount()) return false;
    try {
      final Output oa = ((Model) a).to();
      final Col[] ca = oa.getCols(colNames = oa.checkColNames(colNames)), cb = ((Model) b).to().getCols(colNames);
      for (int i = 0, n = colNames.length; i < n; i++) {//each cols
        final Col x = ca[i], y = cb[i];
        for (int j = 0; j < rows; j++) {//each rows
          final Object p = x.getRow(j), q = y.getRow(j);
          if (!(null == p ? null == q : p.equals(q))) return false;
        }
      }
      return true;
    } catch (RuntimeException e) {//missing data
      return false;
    }
  }

  /*
   * 用於DataPoint轉換時的推算，如Daily Data合併為Weekly或Monthly Data，或LotSum合併為ProdSum
   * <p><pre><code>
   * How to calculate descriptive statistics of [Higher Level]
   * from the descriptive statistics of [Lower  Level] ?
   * [Define] descriptive statistics:
   * 1. N : Sample Size, LowN and HighN
   * 2. A : Average / Mean, LowV and HighV
   * 3. V : Variance, LowV and HighV
   * 4. S : Stdev = Sqrt(V), LowS and HighS
   * [Formula]
   * 1. HighN = Sum(LowN)
   * 2. HighA = Sum(LowA*LowN)/HighN
   * 3. HighV = ($temp-HighA^2*HighN)/(HighN-1)
   * where $temp = Sum( LowN<=1? LowA^2*LowN : (LowA^2+LowS^2)*LowN-LowS^2 )
   * 4. HighS = SqRt(HighV)
   * </code></pre>
   *
   * @param colN     the column name of sample size
   * @param colMean  the column name of mean value
   * @param colStdev the column name of standard deviation value
   * @return String[]{formulaN, formulaMean, formulaStdev}
   * <p>
   * private static String[] getSampleMergedFormula(String colN, String colMean, String colStdev) {
   * final String hn = "SUM(" + colN + ")";
   * final String ha = "*(SUM(*(" + colMean + "," + colN + ")),1/(" + hn + "))";
   * final String ht = "SUM(IIF(<=(" + colN + ",1),*(SQSUM(" + colMean + ")," + colN + "),+(*(SQSUM(" + colMean + "," + colStdev + ")," + colN + "),-(SQSUM(" + colStdev + ")))))";
   * final String hv = "*(+(" + ht + ",-(*(SQSUM(" + ha + ")," + hn + "))),1/(+(" + hn + ",-1)))";
   * return new String[]{"INT(" + hn + ")", ha, "IIF(<=(" + hn + ",1),NA(),SQRT(" + hv + "))"};
   * }
   */

  static boolean createNewFile(File file) throws IOException {
    File parent = file.getParentFile();
    return (null == parent || parent.exists() || parent.mkdirs()) && (!file.exists() || file.delete()) && file.createNewFile();
  }
  /*
  public static QModel doCrossJoin(QModel main, QModel joinModel, String... joinCols) {}
  public static QModel doCrossJoin(QModel main, String joinModelCol, String... joinCols) {}
  public static QModel doUnPivot(QModel model, String keyColName, String valColName, String... flattenCols) {}
  */
}

final class CacheList<T> extends AbstractList {
  private static final Object NULL = MapSet.NULL;
  private final Loader<T> loader;
  private transient Object[] buffer;

  public CacheList(Loader<T> loader, int initialCapacity) {
    super(initialCapacity);
    this.loader = loader;
    this.buffer = new Object[initialCapacity];
  }

  public CacheList(Loader<T> loader) {
    this(loader, 10);
  }

  void grow(int minCapacity) {
    final int oldCapacity = buffer.length;// overflow-conscious code
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity < minCapacity) newCapacity = minCapacity;
    else if (newCapacity > MAX_ARRAY_SIZE) newCapacity = hugeCapacity(minCapacity);
    buffer = Arrays.copyOf(buffer, newCapacity);// minCapacity is usually close to size, so this is a win:
  }

  int getBufferSize() {
    return buffer.length;
  }

  @SuppressWarnings("unchecked")
  public T get(int index) {
    ensureCapacityInternal(index);
    final Object result = buffer[index];
    if (null == result) {//load
      final T value = loader.loadForCache(index);
      buffer[index] = null == value ? NULL : value;
      return value;
    }
    return NULL == result ? null : (T) result;
  }

  @SuppressWarnings("unchecked")
  public T set(int index, T value) {
    ensureCapacityInternal(index);
    final T oldValue = (T) buffer[index];
    buffer[index] = null == value ? NULL : value;
    return oldValue;
  }

  public interface Loader<T> {
    T loadForCache(int index);
  }
}

class IntRead {//treat as a read-only int[]
  final int n;

  IntRead(int n) {
    this.n = n;
  }

  public static IntRead nSize(int size) {
    return new IntRead(size);
  }

  public static IntRead nCopies(int size, int value) {
    return new Const(size, value);
  }

  public static IntRead array(int... ints) {
    return new Array(ints.length, ints);
  }

  public static IntRead arrayOffset(int beginIndex, int length, int... ints) {
    return 0 == beginIndex ? new Array(length, ints) : new ArrayOffset(length, ints, beginIndex);
  }

  public static IntRead mapping(IntRead from, IntRead to) {
    final int n = to.size();
    final IntList result = IntList.create(n);
    for (int i = 0; i < n; i++) {
      final int row = to.get(i);
      result.add(row >= 0 ? from.get(row) : row);
    }
    return result.toRead();
  }

  public final int size() {
    return n;
  }

  public int get(int index) {
    return index;
  }

  public final int getLast(int offset) {
    return get(n - offset);
  }

  public IntRead offset(int offset, int size) {
    return new Offset(size, offset, this);
  }

  public final IntRead offset(int offset) {
    return offset(offset, n);
  }

  public int[] toArray() {
    final int[] result = new int[n];
    for (int i = 0, n = this.n; i < n; i++) result[i] = get(i);
    return result;
  }

  public boolean isSimple() {
    return true;
  }

  @Override
  public int hashCode() {
    return n;
  }

  @Override
  public boolean equals(Object obj) {
    return obj == this || obj instanceof IntRead && equals((IntRead) obj);
  }

  private boolean equals(IntRead ir) {
    if (n != ir.size()) return false;
    for (int i = 0; i < n; i++) if (get(i) != ir.get(i)) return false;
    return true;
  }

  @Override
  public String toString() {
    return size() + getClass().getSimpleName();
  }

  private static final class Offset extends IntRead {
    private final IntRead r;
    private final int o;

    Offset(int n, int o, IntRead r) {
      super(n);
      this.o = o;
      this.r = r;
    }

    @Override
    public IntRead offset(int offset, int size) {
      return new Offset(size, o + offset, r);
    }

    @Override
    public int get(int index) {
      return r.get(index) + o;
    }

    @Override
    public boolean isSimple() {
      return false;
    }

    @Override
    public String toString() {
      return super.toString() + "+" + o;
    }
  }

  private static final class Const extends IntRead {
    private final int value;

    Const(int n, int value) {
      super(n);
      this.value = value;
    }

    @Override
    public int get(int index) {
      return value;
    }

    @Override
    public int[] toArray() {
      final int[] result = new int[n];
      Arrays.fill(result, value);
      return result;
    }

    @Override
    public boolean isSimple() {
      return false;
    }

    @Override
    public String toString() {
      return super.toString() + "=" + value;
    }
  }

  private static class Array extends IntRead {
    final int[] ints;

    Array(int n, int[] ints) {
      super(n);
      this.ints = ints;
    }

    public int get(int index) {
      return ints[index];
    }

    public int[] toArray() {
      return Arrays.copyOf(ints, n);
    }

    @Override
    public final boolean isSimple() {
      return false;
    }

    @Override
    public String toString() {
      return super.toString() + "@" + ints;
    }
  }

  private static final class ArrayOffset extends Array {//slower
    private final int o;

    ArrayOffset(int n, int[] ints, int offset) {
      super(n, ints);
      this.o = offset;
    }

    @Override
    public int get(int index) {
      return ints[index + o];
    }

    @Override
    public int[] toArray() {
      return Arrays.copyOfRange(ints, o, o + n);
    }

    @Override
    public String toString() {
      return super.toString() + "+" + o;
    }
  }
}
